/**
 * 
 * @Project le cOeur - Data Virtualization
 * @Date 08.12.2012
 * @Author Aebi Pascal, Kocher Marc
 * @School BFH-TI Biel, Bachelor Thesis
 *
 */

package main.api;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import java.util.logging.Logger;

import main.api.connection.Protocol;
import main.api.connection.SocketFactory;
import main.api.http.ContentType;
import main.api.http.MessageBody;
import main.api.http.MessageHeader;
import main.api.http.Method;
import main.api.http.Request;
import main.api.http.Response;
import main.api.http.Version;
import main.api.log.LoggerCreator;
import main.api.security.KeyStoreManager;
import main.api.setting.Config;

/**
 * This class represents the HTTP client.
 */
public class HTTPClient {
	
	private KeyStoreManager keyStoreManager;
	private SocketFactory socketFactory;
	private RequestFactory requestFactory;
	private Logger logger;
	
	/**
	 * Creates a HTTP client.
	 */
	public HTTPClient() {
		LoggerCreator loggerCreator = new LoggerCreator();
		try {
			this.logger = loggerCreator.createLogger("SysLogger", Level.INFO, "client.log");
		} catch (SecurityException e) {
			e.printStackTrace();
			System.exit(1);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
		
		try {
			this.keyStoreManager = new KeyStoreManager();
			this.keyStoreManager.initKeyStores();
			this.socketFactory = new SocketFactory(this.keyStoreManager);
			this.requestFactory = new RequestFactory();
		} catch (UnrecoverableKeyException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		} catch (KeyStoreException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		} catch (NoSuchAlgorithmException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		} catch (CertificateException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		} catch (FileNotFoundException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		} catch (IOException e) {
			this.logger.severe(e.getMessage());
			System.exit(1);
		}
	}
	
	/**
	 * Establishes a connection with the server of the given URI and sends him a HTTP GET request.
	 * Afterwards it receives the corresponding response form the server and gives the message back.
	 * 
	 * @param uri The requested URI.
	 * @return The HTTP response.
	 * @throws URISyntaxException If the given string violates RFC 2396, as augmented by the above deviations.
	 * @throws KeyManagementException If the init operation fails.
	 * @throws NoSuchAlgorithmException If no Provider supports a TrustManagerFactorySpi implementation for the specified protocol.
	 * @throws UnknownHostException If the host is not known.
	 * @throws IOException If an I/O error occurs.
	 */
	protected Response get(String uri) throws URISyntaxException, KeyManagementException, NoSuchAlgorithmException, UnknownHostException, IOException {
		URI requestedURI = new URI(uri);
		
		Protocol protocol = Protocol.HTTP;
		if(Config.SECURE_CONNECTION.equals("1")){
			protocol = Protocol.TLS;
		}
		
		Socket socket = this.socketFactory.createSocket(protocol, requestedURI.getHost(), requestedURI.getPort());
		
		Request request = this.requestFactory.createRequest(Method.GET, requestedURI, Version.HTTP_1_1);
		
		this.sendRequest(socket, request);
		
		return this.receiveResponse(socket);
	}
	
	/**
	 * Makes a HTTP PUT request.
	 * Not yet implemented.
	 * 
	 * @param uri The requested URI.
	 * @param data The data.
	 */
	protected void put(String uri, String data) {
		// TODO: implement function
	}
	
	/**
	 * Makes a HTTP POST request.
	 * Not yet implemented.
	 * 
	 * @param uri The requested URI.
	 * @param data The data.
	 */
	protected void post(String uri, String data) {
		// TODO: implement function
	}
	
	/**
	 * Makes a HTTP DELETE request.
	 * Not yet implemented.
	 * 
	 * @param uri The requested URI.
	 */
	protected void delete(String uri) {
		// TODO: implement function
	}
	
	/**
	 * Receives incoming data from the socket, parses them and returns a HTTP response object.
	 * 
	 * @param socket The socket.
	 * @return The received HTTP response.
	 * @throws IOException If an I/O error occurs.
	 */
	private Response receiveResponse(Socket socket) throws IOException {
		if(!socket.isClosed()){
			InputStream inputStream = socket.getInputStream();
			InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
			BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
			Response response = null;
			String line;
			int length = 0;
			boolean receivingRequest = true;
			boolean isHeader = false;
			// Receive data and compose request
			while(receivingRequest){
				line = bufferedReader.readLine();
				if (line == null){
					throw new IOException("End of input stream is reached.");
				} 
				else if(line.isEmpty()){
					isHeader = false;
					response.parse();
					// Check content length and type
					MessageHeader contentLength = response.getMessageHeader("Content-Length");
					MessageHeader contentType = response.getMessageHeader("Content-Type");
					if(contentLength != null && contentType != null){
						length = Integer.parseInt(contentLength.getFieldValue());
						// Read and set data
						if(length > 0){
							char[] messageBody = new char[length];
							bufferedReader.read(messageBody, 0, length);
							response.addMessageBody(new MessageBody(new String(messageBody), new ContentType(contentType.toString())));
						}
					}
					receivingRequest = false;
				}
				else if(isHeader){
					response.addMessageHeader(new MessageHeader(line));
				}
				else{
					response = new Response(line);
					isHeader = true;
				}
			}
			return response;
		}
		else{
			return null;
		}
	}
	
	/**
	 * Sends a given HTTP request over the socket.
	 * 
	 * @param socket The socket.
	 * @param request The HTTP request.
	 * @throws IOException If an I/O error occurs.
	 */
	private void sendRequest(Socket socket, Request request) throws IOException {
		if(!socket.isClosed()){
			OutputStream outputStream = socket.getOutputStream();
			OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
			BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
			bufferedWriter.write(request.toString());
			bufferedWriter.flush();
		}
	}

}

